📝 我的笔记

还没有笔记

选中页面文字后点击「高亮」按钮添加

1 内存管理

📜 原文
📖 逐步解释
∑ 公式拆解
💡 数值示例
⚠️ 易错点
📝 总结
🎯 存在目的
🧠 直觉心智模型
💭 直观想象

1内存管理

mallocfree

| 高地址 ⟶
yout | 命令行参数和环境变量 |

| :--- | :--- |

| | 栈 |

| W
ey are: | |

| | |

| | 堆 |

| | 未初始化数据 (bss) |

| | 初始化数据 |

| | 只读数据 |

| 低地址 ⟶ | 文本 |

这里存放着程序编译后的代码,作为独立的 CPU 指令

通常你的代码没有理由在这里进行读/写操作,并且出于安全原因,它通常是只读的。

| 命令行参数和环境变量 |

| :--- |

| 栈 |

| |

| 堆 |

| 未初始化数据 (bss) |

| 初始化数据 |

| 只读数据 |

| |

2只读数据

const 变量字符串字面量存储在只读数据区。

高地址 ⟶

低地址 ⟶

3初始化数据

包含由程序员初始化的全局变量静态变量

不是只读的,因为变量的值可以在运行时被改变。

4未初始化数据

称为 bss 或 block started by symbol(符号起始块)。

此段中的数据在程序开始执行前由内核初始化为算术 0。

包含所有初始化为零或在源代码中没有显式初始化的全局变量静态变量

“更好地节省空间”——尝试最小化进入此区域的变量数量。

5

这一部分完全由程序员使用。

通常用于可变大小数组和在程序运行期间创建的大型对象

由于这种“自由”,它需要手动管理,并且容易出现内存错误内存泄漏

低地址 ⟶

6

这一部分是自动管理的,存储你的变量和参数。

每次调用函数或创建局部变量时它都会增长,而当函数返回或变量超出作用域时它会缩小。

+ 非常安全,易于使用

7为什么需要堆?

假设你想编写一个函数来在上创建并返回一个整数数组。它可能看起来像这样:

```

int *get_int_array() {

// stack-allocated array

int a[3] = {1, 2, 3};

return a;

}

int *p = get_int_array();

```

8为什么需要堆?

让我们看看这段代码运行时的示意图:

```

int *get_int_array() {

// stack-allocated array

int a[3] = {1, 2, 3};

return a;

}

int *p = get_int_array();

```

9为什么需要堆?

让我们看看这段代码运行时

栈帧

| main function | |

| :--- | :---: |

| int argc | |

| int argv | |

| int p | |

```

int *get_int_array() {

// stack-allocated array

int a[3] = {1, 2, 3};

return a;

}

int *p = get_int_array();

```

get_int_array() 返回后,栈会收缩,因此 p 现在指向一个无效内存地址

10malloc()

malloc 允许程序员在上分配内存。

它唯一的参数是要在上分配的连续字节数

成功时,malloc 返回指向已分配区域起始的指针;失败时,返回 NULL

11free()

free 释放上先前malloc分配的内存区域。

它唯一的参数是指向要释放区域起始的指针。

当程序重复调用 malloc 而不释放已分配的内存时,的大小会增长,程序最终会崩溃。分配指针后从未释放它被称为内存泄漏

12Malloc 示例

这是使用 malloc 修改后的相同函数:

```

int *get_int_array() {

// heap-allocated array

int a = malloc(3 sizeof(int));

a[0] = 1; a[1] = 2; a[2] = 3;

return a;

}

int *p = get_int_array();

// use p to access the array

free (p);

```

13Malloc 示例

| main function int argc int argv | |

| :--- | :--- |

| | 调用 |

| get_int_array int *a; | |

| | |

| | |

14Malloc 示例

```

int *get_int_array() {

// heap-allocated array

int a = malloc(3 sizeof(int));

a[0] = 1; a[1] = 2; a[2] = 3;

return a;

}

```

```

int *p = get_int_array();

// use p to access the array

free (p);

```

栈帧

15Malloc 示例

```

int *get_int_array() \{

// heap-allocated array

int a = malloc(3 sizeof(int));

a[0] = 1; a[1] = 2; a[2] = 3;

return a;

\}

int *p = get_int_array();

// use p to access the array

free (p);

```

栈帧

16Malloc 示例

```

int *get_int_array() {

// heap-allocated array

int a = malloc(3 sizeof(int));

a[0] = 1; a[1] = 2; a[2] = 3;

return a;

}

int *p = get_int_array();

// use p to access the array

```

free (p);

17栈帧

| main function | |

| :--- | :--- |

| int argc | |

| int argv | |

| int *p | |

| | |

18Valgrind

Valgrind 是一个命令行工具,用于报告可执行文件中的内存错误内存泄漏。要使用 valgrind,运行

$$ Yalgrind --leak-check=full . executable name arg1 arg2

Valgrind 报告的内存错误包括读取未初始化数据、写入未分配内存位置、解引用已释放的指针以及尝试多次释放同一指针。

19理解 Valgrind 错误消息

内存泄漏发生在堆内存未被释放时,通常不会导致任何异常行为(除非进程耗尽可分配内存)。

相反,内存错误未定义行为的常见来源。

有四种你应该理解的错误消息:

20无效读取

当你尝试从程序不可用的内存位置读取值时,会发生无效读取(例如,malloced 块外部的堆内存以及超出顶的内存)。

错误消息总是包含你的程序尝试读取的字节数,这有助于调试错误。

BB 服务器上,大小为 1 表示 char,4 表示 int,8 通常表示指针。参考所有数据类型的大小,这些信息将帮助你调试代码。

21无效读取

这是一个简单的程序,它将通过读取堆分配整数数组之外的 4 个字节(一个额外的 int)导致大小为 4 的无效读取。程序分配了 20 个字节,同时尝试解引用并从 24 个字节处读取。

```

int main(void) {

int p = malloc(5 sizeof(int));

if (p == NULL) {

exit(1);

}

for (int i = 0; i < 5; i++) {

p[i] = i;

}

for (int i = 0; i < 6; i++) {

printf("%d\n", p[i]);

}

free(p);

}

```

22无效写入

无效写入类似于无效读取,但它发生在你尝试写入非法内存位置时。

这是一个简单的程序,它通过在堆分配数组之外写入一个额外的 int 导致大小为 4 的无效写入。此程序分配 20 个字节(5 个 int),但尝试写入 24 个字节的值(6 个 int)。

```

int main(void) {

int p = malloc(5 sizeof(int));

if (p == NULL) {

exit(1);

}

for (int i = 0; i < 6; i++) {

p[i] = i;

}

for (int i = 0; i < 5; i++) {

printf("%d\n", p[i]);

}

free(p);

}

```

23依赖于未初始化值的条件跳转或移动

错误消息“conditional jump or move depends on uninitialized value(s)”(条件跳转移动依赖于未初始化值)表明某个操作的结果依赖于未初始化变量

由于未初始化变量的值未定义,因此结果依赖于未初始化变量的程序将具有不确定行为

条件跳转是指决定控制流的语句,例如 if 语句、while 循环或 for 循环。

移动是指从内存中进行任何其他类型的读取。

请注意,如果你将指向未初始化内存的指针传递给 printf() 等库函数,条件跳转移动可能会发生在库代码深处。

24依赖于未初始化值的条件跳转或移动

在下面的程序中,h() 将指向未初始化变量 $x$ 的指针传递给 g()g() 解引用该指针。g() 读取的值通常是不可预测的;在这种情况下,g() 可能会拾取 f()栈帧留下的值 42。

```

#include

void f(void) {

int x = 42;

printf("f: %p -> %d\n", &x, x);

}

void g(int *i) {

int j = *i + 10;

printf("g: %d\n", j);

}

void h(void) {

int x; // Uninitialized

g(&x);

}

int main(void) {

f();

h();

}

```

25依赖于未初始化值的条件跳转或移动

当你忘记空终止字符串时,通常会发生此错误。

这是一个示例程序,它填充并打印 char 数组 a。但是,它在将其传递给 printf() 之前忘记空终止 a,从而导致内存错误

```

int main(void) {

char *s = "hello";

char a[6];

for (int i = 0; i < 5; i++) {

a[i] = s[i];

}

printf("%s\n", a);

}

```

26段错误

当操作系统在无效读取写入后介入,导致程序崩溃时,会发生段错误段错误最常见的来源是尝试解引用空指针,表现为在 $0 \times 0$ 处的无效读取段错误可以在不使用 valgrind 的情况下观察到,但 valgrind 将提供有关错误发生位置和原因的更详细信息。

27段错误

这是一个尝试写入只读内存的程序。char *s 指向内存的只读代码区,但 char 数组 s1s2 位于上,可以从中读取和写入。第一次调用 strcpy()s 读取并写入 s1,这没问题。然而,第二次调用尝试写入 s,这会导致段错误

```

int main(void) {

char *s = "hi";

char s1[3] = "no";

char s2[3] = "ok";

printf("%s %s %s\n", s, s1, s2);

strcpy(s1, s); // This is OK.

printf("%s %s %s\n", s, s1, s2);

strcpy(s, s2); // This is not.

printf("%s %s %s \n", s, s1, s2);

}

```